package it.crudity.core.dao.impl;

import it.crudity.core.dao.GenericDao;
import it.crudity.core.model.BaseEntity;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;

import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.orm.hibernate3.HibernateTemplate;

@SuppressWarnings({"unchecked","rawtypes"})
@Scope("prototype")
public class GenericDaoHibernate<T extends BaseEntity<ID>, ID extends Serializable> implements GenericDao<T, ID> {

	protected Class<T> domainModelObjectClass;
	protected Class<ID> idClass;

	protected HibernateTemplate hibernateTemplate;

	/***
	 * Default constructor. Can only be used by subclasses. Sets the domain
	 * classes using superclass's type arguments.
	 */
	protected GenericDaoHibernate() {
		java.lang.reflect.ParameterizedType t = (ParameterizedType) this.getClass().getGenericSuperclass();
		domainModelObjectClass = (Class) t.getActualTypeArguments()[0];
		idClass = (Class) t.getActualTypeArguments()[1];
	}

	/***
	 * Instantiates a {@link GenericDao} for the given entity and entity's id
	 * classes.
	 * 
	 * @param clazz
	 *            Entity class
	 * @param idClazz
	 *            Entity's id class
	 */
	public GenericDaoHibernate(Class<T> clazz, Class<ID> idClazz) {
		domainModelObjectClass = clazz;
		idClass = idClazz;
	}

	/***
	 * {@link Autowired} property. Sets the Hibernate Session Factory
	 * 
	 * @param sessionFactory
	 */
	@Autowired
	public void setSessionFactory(SessionFactory sessionFactory) {
		hibernateTemplate = new HibernateTemplate(sessionFactory);
	}

	public T get(ID id) {
		return (T) hibernateTemplate.get(domainModelObjectClass, id);
	}

	public void saveOrUpdate(T object) {
		hibernateTemplate.saveOrUpdate(object);
	}

	public void delete(T object) {
		hibernateTemplate.delete(object);
	}

	public List<T> getAll() {
		List<T> result = hibernateTemplate.loadAll(domainModelObjectClass);
		return result;
	}

	public List<T> findByProperty(String property, Object value) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		crit.add(Property.forName(property).eq(value));
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findByPropertyIn(String property, Collection<? extends Object> values) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		crit.add(Property.forName(property).in(values));
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findAscendingOrderedByProperty(String property, Object value, String... orderingFields) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		crit.add(Property.forName(property).eq(value));
		for (String orderingProperty : orderingFields) {
			crit.addOrder(Order.asc(orderingProperty));
		}
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findByStringPropertyLike(String property, String value, StringMatchMode matchMode) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		crit.add(Restrictions.ilike(property, value, getMatchMode(matchMode)));
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findByProperties(String[] properties, Object[] values) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		for (int i = 0; i < properties.length; i++)
			crit.add(Property.forName(properties[i]).eq(values[i]));
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findByStringPropertiesLike(String[] properties, String[] values, StringMatchMode[] matchModes) {
		DetachedCriteria crit = DetachedCriteria.forClass(domainModelObjectClass);
		for (int i = 0; i < properties.length; i++)
			crit.add(Restrictions.ilike(properties[i], values[i], getMatchMode(matchModes[i])));
		return hibernateTemplate.findByCriteria(crit);
	}

	public List<T> findByExample(T example) {
		return hibernateTemplate.findByExample(example);
	}

	/***
	 * Used to get a single element in a list returned by hibernate.
	 * 
	 * @param list
	 *            the list
	 * @return the only object in the list
	 * @throws RuntimeException
	 *             if the list is null or has 0 or 2+ elements
	 */
	protected T returnUnique(List<T> list) {
		if (list == null || list.size() == 0)
			return null;
		if (list.size() == 1)
			return list.get(0);
		if (list.size() > 1)
			throw new RuntimeException("returnUnique(list) called on a list with " + list.size() + " elements");
		return null;

	}

	public T getForAssociation(ID id) {
		return (T) hibernateTemplate.load(domainModelObjectClass, id);
	}

	public void makeTransient(T entity) {
		hibernateTemplate.evict(entity);
	}

	private static org.hibernate.criterion.MatchMode getMatchMode(StringMatchMode matchMode) {
		switch (matchMode) {
		case ANYWHERE:
			return org.hibernate.criterion.MatchMode.ANYWHERE;
		case START:
			return org.hibernate.criterion.MatchMode.START;
		case END:
			return org.hibernate.criterion.MatchMode.END;
		case EXACT:
			return org.hibernate.criterion.MatchMode.EXACT;
		}
		return null;
	}

	public Class<T> getDomainModelObjectClass() {
		return domainModelObjectClass;
	}

	public Class<ID> getIdClass() {
		return idClass;
	}

}